在這個已經是大部分開發工程師都在用 ChatGPT、GitHub Copilot、Claude 的時代,可能有人會問:「既然 AI 都能自動產生測試程式碼了,為什麼還要花時間學習怎麼寫測試呢?」
這也讓我看到很多開發工程師在沒有學習與實作過單元測試的歷程下,使用 AI 去產生了實作程式碼以及測試程式碼,但只知道有產生測試程式碼而且都有執行都有綠燈通過,然後連測試程式碼是在測試什麼、驗證什麼情境、測試程式碼是什麼結構都完全不去瞭解。
但真的出現了程式 Bug 或是需求調整時,也都是輸入 Prompt 後讓 AI 去做修改與調整,然後就一直無限循環下去,做到最後去問他們這些程式碼寫了什麼、有沒有符合情境、測試是測了哪些,對這一切完全模模糊糊,只會說這都是 AI 產生的,他什麼都不知道...
AI 可以幫你寫程式碼,但不能幫你思考。
生成式 AI 確實讓測試程式碼的產生變得容易,但也帶來了新的危機:
作為一個在軟體領域打滾十多年的「老派」工程師,我深信:在 AI 時代,理解測試的本質比會寫測試程式碼更重要。
這個系列不是要教你寫程式碼(AI 已經很會了),而是要培養你的測試思維、策略規劃能力,以及最重要的——如何善用 AI 而不被 AI 誤導。
這一系列將主要是以 C# 及 .NET Core 為開發環境
身為一個在軟體開發領域打滾多年的工程師,我必須承認:在職業生涯的前幾年,我對所開發程式碼的態度就是「能跑就好」。當時的我認為,只要程式能正常執行、使用者能正常操作,就算是成功的軟體了。
而所謂的 測試
就是將環境執行起來,然後將各種可能的使用情境都操作過一遍,盡可能地把程式可能發生錯誤的情況給找出來,然後修改程式碼之後,前面的步驟與流程再走一遍,如此一直到功能交付的期限都是這樣的循環。
直到某一天...
當系統推上線並開始正式服務後,各種未預期的狀況隨之而來。各種錯誤的情況都是之前那些 測試
都沒有碰到過的狀況,各種錯誤的狀況越來越多,最後導致服務出現了中斷甚至影響的營運。
很多人說出口的永遠都是那些話:
那一刻我才意識到:沒有測試的程式碼,就像是沒有安全網的高空走鋼絲。
而所謂的 測試
不是那種 人工、手動、繁瑣地
的步驟與流程,而是 單元測試
、自動化測試
。
很多人以為測試的目的就是找 bug,但其實測試的真正價值在於:
在開始寫測試之前,我們需要了解一個重要的概念:測試金字塔。
這是在接觸測試時,第一個必須要理解的概念:
端對端測試(E2E Tests) - 金字塔的頂端
整合測試(Integration Tests) - 金字塔的中層
單元測試(Unit Tests) - 金字塔的基座
測試金字塔的形狀反映了測試的投資報酬率:
// 單元測試:快速、可靠、容易除錯(<100ms)
[Test]
public void Calculate_輸入有效金額和折扣率_應回傳正確折扣金額()
{
var calculator = new DiscountCalculator();
var result = calculator.Calculate(100, 0.1);
Assert.AreEqual(10, result); // 通常 1-5ms 完成
}
// 整合測試:測試真實互動,但較慢(1-10秒)
[Test]
public void Save_輸入有效訂單資料_應成功儲存至資料庫()
{
var repository = new OrderRepository(_connectionString);
var order = new Order { Amount = 100, CustomerId = 1 };
var id = repository.Save(order); // 需要 1-2 秒
Assert.IsTrue(id > 0);
}
// E2E 測試:完整流程,但最慢且最脆弱(30秒-幾分鐘)
[Test]
public void PlaceOrder_完整下單流程_應成功完成訂單()
{
// 啟動瀏覽器、登入、選擇商品、結帳...
// 可能需要 30 秒到幾分鐘
// 任何一個環節出問題都會失敗
}
直到現在自動化、AI 自動化流程盛行下,依然還是有很多公司、開發團隊還在做一件事情:就是過度依賴 人工、手動、繁瑣地
的傳統測試。
結果是:
現在我的建議是:從底部開始建立,穩固基礎再往上層發展。
隨著 GitHub Copilot、ChatGPT 等 AI 工具的普及,現在產生測試程式碼變得前所未有的容易。但這也帶來了新的問題:
// X - AI 可能會產生這樣的測試:表面看起來正確
[Test]
public void TestCalculateTotal()
{
var calculator = new Calculator();
var result = calculator.CalculateTotal(100, 0.1);
Assert.AreEqual(110, result);
}
// O - 經過思考的測試:遵循 FIRST 原則和 3A Pattern
[Test]
public void CalculateTotal_輸入有效金額和稅率_應回傳正確總額()
{
// Arrange
var calculator = new Calculator();
var amount = 100m;
var taxRate = 0.1m;
var expected = 110m;
// Act
var result = calculator.CalculateTotal(amount, taxRate);
// Assert
Assert.AreEqual(expected, result);
}
這個對比說明了什麼?第一個測試雖然能跑,但:
AI 可以幫你寫程式碼,但不能幫你思考測試策略。
為了寫出真正有價值的測試,我們需要遵循 FIRST 原則。這是我在公司內部推動測試文化時,首先教給團隊的核心概念:
每個單元測試的執行時間要很短
// X - 慢速測試:每次都連接資料庫
[Test]
public void SaveUser_ShouldReturnId()
{
var connection = new SqlConnection("...");
var repository = new UserRepository(connection);
var user = new User { Name = "John" };
var id = repository.Save(user); // 實際 DB 操作,慢!
Assert.IsTrue(id > 0);
}
// O - 快速測試:使用 NSubstitute
[Test]
public void SaveUser_輸入有效使用者資料_應回傳使用者ID()
{
var mockRepository = Substitute.For<IUserRepository>();
mockRepository.Save(Arg.Any<User>()).Returns(123);
var service = new UserService(mockRepository);
var result = service.CreateUser("John");
Assert.AreEqual(123, result.Id);
}
每個單元測試都是獨立的,不會和其他單元測試相依,也不會和外部資源相依
// X - 相依的測試:順序很重要
public class OrderTests
{
private static Order _testOrder;
[Test, Order(1)]
public void CreateOrder_ShouldSucceed()
{
_testOrder = new Order { Amount = 100 };
Assert.IsNotNull(_testOrder);
}
[Test, Order(2)]
public void ProcessOrder_ShouldUpdateStatus()
{
_testOrder.Process(); // 依賴前一個測試!
Assert.AreEqual(OrderStatus.Processed, _testOrder.Status);
}
}
// O - 獨立的測試:每個測試都自給自足
public class OrderTests
{
[Test]
public void CreateOrder_輸入有效金額_應成功建立訂單()
{
var order = new Order { Amount = 100 };
Assert.IsNotNull(order);
}
[Test]
public void ProcessOrder_輸入有效訂單_應更新訂單狀態()
{
var order = new Order { Amount = 100 }; // 自己建立需要的資料
order.Process();
Assert.AreEqual(OrderStatus.Processed, order.Status);
}
}
// X - 相依外部資源:依賴檔案系統、資料庫、網路服務
public class UserServiceTests
{
[Test]
public void SaveUser_ShouldReturnUserId()
{
// 依賴真實資料庫連線
var connectionString = "Server=localhost;Database=TestDB;";
var repository = new UserRepository(connectionString);
var service = new UserService(repository);
// 依賴檔案系統
var configFile = @"C:\config\settings.json";
var config = File.ReadAllText(configFile);
// 依賴外部 API
var httpClient = new HttpClient();
var response = httpClient.GetAsync("https://api.example.com/validate").Result;
var user = new User { Name = "John", Email = "john@example.com" };
var result = service.SaveUser(user);
Assert.IsTrue(result > 0);
}
}
// O - 獨立於外部資源:使用 Mock 物件和測試替身
public class UserServiceTests
{
[Test]
public void SaveUser_輸入有效使用者_應回傳使用者ID()
{
// 使用 Mock 取代真實資料庫
var mockRepository = Substitute.For<IUserRepository>();
mockRepository.SaveUser(Arg.Any<User>()).Returns(123);
// 使用 Mock 取代檔案讀取
var mockFileService = Substitute.For<IFileService>();
mockFileService.ReadConfig().Returns("test-config");
// 使用 Mock 取代 HTTP 呼叫
var mockHttpService = Substitute.For<IHttpService>();
mockHttpService.ValidateUser(Arg.Any<string>()).Returns(true);
var service = new UserService(mockRepository, mockFileService, mockHttpService);
var user = new User { Name = "John", Email = "john@example.com" };
var result = service.SaveUser(user);
Assert.AreEqual(123, result);
}
}
每個單元測試不論在任何環境都可以重複執行
// X - 不可重複:依賴當前時間
[Test]
public void IsBusinessDay_TodayShould_ReturnTrue()
{
var service = new DateService();
var result = service.IsBusinessDay(DateTime.Now); // 週末會失敗!
Assert.IsTrue(result);
}
// O - 可重複:明確指定測試資料
[Test]
public void IsBusinessDay_輸入星期一_應回傳True()
{
var service = new DateService();
var monday = new DateTime(2024, 1, 1); // 2024/1/1 是星期一
var result = service.IsBusinessDay(monday);
Assert.IsTrue(result);
}
// X - 不可重複:依賴環境變數和隨機數
[Test]
public void ProcessOrder_ShouldGenerateOrderNumber()
{
// 依賴環境變數,不同環境會有不同結果
var environment = Environment.GetEnvironmentVariable("ORDER_PREFIX") ?? "ORDER";
// 依賴隨機數,每次執行結果都不同
var random = new Random();
var orderNumber = $"{environment}-{random.Next(1000, 9999)}";
var order = new Order { Number = orderNumber };
var service = new OrderService();
var result = service.ProcessOrder(order);
// 這個測試會因為環境不同或執行次數而失敗
Assert.AreEqual("ORDER-1234", result.OrderNumber);
}
// X - 不可重複:依賴全域狀態和執行順序
public class CounterTests
{
private static int _globalCounter = 0;
[Test]
public void IncrementCounter_ShouldReturnOne()
{
_globalCounter++; // 依賴全域狀態
Assert.AreEqual(1, _globalCounter); // 第二次執行會失敗
}
[Test]
public void IncrementCounter_ShouldReturnTwo()
{
_globalCounter++; // 執行順序會影響結果
Assert.AreEqual(2, _globalCounter); // 順序改變就會失敗
}
}
// O - 可重複:使用固定值和隔離的測試資料
[Test]
public void ProcessOrder_輸入固定前綴_應產生正確訂單編號()
{
// 使用固定的測試資料,不依賴環境
var orderPrefix = "TEST";
var orderNumber = "12345";
var expectedOrderNumber = $"{orderPrefix}-{orderNumber}";
var order = new Order { Prefix = orderPrefix, Number = orderNumber };
var service = new OrderService();
var result = service.ProcessOrder(order);
// 每次執行都會得到相同結果
Assert.AreEqual(expectedOrderNumber, result.OrderNumber);
}
// O - 可重複:每個測試都有獨立的資料
public class CounterTests
{
[Test]
public void IncrementCounter_從0開始_應回傳1()
{
var counter = new Counter(); // 每個測試都建立新的實例
counter.Increment();
Assert.AreEqual(1, counter.Value);
}
[Test]
public void IncrementCounter_從0開始_應回傳2()
{
var counter = new Counter(); // 獨立的實例,不受其他測試影響
counter.Increment();
counter.Increment();
Assert.AreEqual(2, counter.Value);
}
}
當單元測試執行完之後就能得知測試結果
// X - 需要人工檢查
[Test]
public void GenerateReport_ShouldCreateFile()
{
var generator = new ReportGenerator();
generator.GenerateReport();
// 需要手動檢查檔案是否存在和內容是否正確
}
// O - 自動驗證
[Test]
public void GenerateReport_輸入有效資料_應建立包含正確內容的檔案()
{
var generator = new ReportGenerator();
var result = generator.GenerateReport();
Assert.IsTrue(File.Exists(result.FilePath));
Assert.IsTrue(result.Content.Contains("Total Sales"));
Assert.IsTrue(result.Content.Length > 0);
}
// X - 驗證錯誤時訊息不明確
[Test]
public void CalculatePrice_ShouldBeCorrect()
{
var calculator = new PriceCalculator();
var result = calculator.Calculate(100, 0.1);
Assert.IsTrue(result > 0); // 失敗時只顯示:Expected: True, Actual: False
}
// O - 驗證錯誤時提供清楚的錯誤訊息
[Test]
public void CalculatePrice_輸入100元和10%折扣_應回傳90元()
{
var calculator = new PriceCalculator();
var basePrice = 100m;
var discount = 0.1m;
var expected = 90m;
var result = calculator.Calculate(basePrice, discount);
// 失敗時顯示:Expected: 90, Actual: 85,清楚知道期望值和實際值
Assert.AreEqual(expected, result,
$"計算價格錯誤。基礎價格: {basePrice}, 折扣: {discount:P}, 期望: {expected}, 實際: {result}");
}
// O - 提供具體的驗證訊息和檢查多個條件
[Test]
public void CreateUser_輸入有效資料_應建立完整使用者物件()
{
var service = new UserService();
var userName = "john.doe";
var email = "john@example.com";
var result = service.CreateUser(userName, email);
// 每個驗證都有明確的錯誤訊息
Assert.IsNotNull(result, "建立的使用者物件不應為 null");
Assert.AreEqual(userName, result.UserName, $"使用者名稱應為 '{userName}'");
Assert.AreEqual(email, result.Email, $"Email 應為 '{email}'");
Assert.IsTrue(result.Id > 0, "使用者 ID 應大於 0");
Assert.IsTrue(result.CreatedDate <= DateTime.Now, "建立時間不應晚於現在");
// 測試失敗時會顯示類似:
// Expected: john.doe, Actual: john_doe
// 使用者名稱應為 'john.doe'
}
// X - 複雜的驗證邏輯,失敗時難以理解原因
[Test]
public void ValidateOrder_ShouldPassAllRules()
{
var order = new Order
{
Amount = 50,
CustomerType = "Regular",
Items = new[] { "Item1", "Item2" }
};
var validator = new OrderValidator();
var isValid = validator.Validate(order);
Assert.IsTrue(isValid); // 失敗時無法知道違反了哪個規則
}
// O - 分別驗證各個條件,提供清楚的錯誤訊息
[Test]
public void ValidateOrder_輸入有效訂單_應通過所有驗證規則()
{
var order = new Order
{
Amount = 150,
CustomerType = "VIP",
Items = new[] { "Item1", "Item2", "Item3" }
};
var validator = new OrderValidator();
var result = validator.ValidateDetailed(order);
Assert.IsTrue(result.IsValid, $"訂單驗證失敗:{string.Join(", ", result.Errors)}");
Assert.IsTrue(result.AmountValid, $"金額驗證失敗:最低金額為100,實際金額為{order.Amount}");
Assert.IsTrue(result.CustomerTypeValid, $"客戶類型驗證失敗:'{order.CustomerType}' 不在允許的類型中");
Assert.IsTrue(result.ItemCountValid, $"商品數量驗證失敗:最少需要2項商品,實際為{order.Items.Length}項");
// 失敗時會顯示具體違反的規則和相關數值
}
單元測試應該在產品程式碼完成的當下就可以驗證執行結果是否符合預期
當我們說測試要「及時」,不僅僅是指要做 TDD(測試驅動開發),更重要的是:
立即可測試性:
無依賴執行:
專注性驗證:
開發流程的順暢性:
寫程式碼 → 寫測試 → 執行測試 → 立即知道結果 → 繼續開發
而不是:
寫程式碼 → 部署到測試環境 → 設定資料庫 → 準備測試資料 → 啟動服務 → 手動測試 → 發現問題再回頭找原因
範例程式碼:
// X - 不及時:需要複雜環境才能測試
public class OrderService
{
public void ProcessOrder(Order order)
{
// 需要連接真實資料庫
// 需要外部 API 服務運行
// 需要特定的設定檔
// 需要預先準備測試資料
// 要測試這個方法,需要:
// 1. 啟動資料庫服務
// 2. 設定連線字串
// 3. 建立測試用的客戶資料
// 4. 確保外部 API 可用
// 5. 部署到測試環境
// 光是準備測試環境就需要很多時間
}
}
// O - 及時:完成實作後立即可測試
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IPaymentService _paymentService;
public OrderService(IOrderRepository repository, IPaymentService paymentService)
{
_repository = repository;
_paymentService = paymentService;
}
public OrderResult ProcessOrder(Order order)
{
if (order == null) return OrderResult.Invalid("Order cannot be null");
if (order.Amount <= 0) return OrderResult.Invalid("Amount must be positive");
var paymentResult = _paymentService.ProcessPayment(order.Amount);
if (!paymentResult.Success) return OrderResult.Failed("Payment failed");
var savedOrder = _repository.Save(order);
return OrderResult.Success(savedOrder.Id);
}
}
// 立即可測試:不需要任何外部相依
[Test]
public void ProcessOrder_輸入有效訂單_應成功處理()
{
// 使用 Mock,不需要真實資料庫或外部服務
var mockRepository = Substitute.For<IOrderRepository>();
var mockPaymentService = Substitute.For<IPaymentService>();
mockPaymentService.ProcessPayment(100).Returns(PaymentResult.Success());
mockRepository.Save(Arg.Any<Order>()).Returns(new Order { Id = 123 });
var service = new OrderService(mockRepository, mockPaymentService);
var order = new Order { Amount = 100 };
// 立即執行測試,馬上知道結果
var result = service.ProcessOrder(order);
Assert.IsTrue(result.IsSuccess);
Assert.AreEqual(123, result.OrderId);
// 整個測試過程不到 1 秒,不需要任何外部準備
}
延遲測試的痛點:
及時測試的優勢:
除了 FIRST 原則,還有一個簡單但強大的模式:3A Pattern
一個單元測試方法裡所包含的三個行為
[Test]
public void CalculateTotal_WithTax_ShouldAddTaxToAmount()
{
// Arrange:準備測試資料
var calculator = new TaxCalculator();
var amount = 100m;
var taxRate = 0.1m;
// Act:執行要測試的動作
var result = calculator.CalculateTotal(amount, taxRate);
// Assert:驗證結果
Assert.AreEqual(110m, result);
}
Arrange:
sut (system under test)
Act:
actual
存放回傳值Assert:
這個模式讓測試的結構清晰明瞭,任何人都能快速理解測試的意圖。
除了測試的結構要清晰,測試方法的命名也同樣重要。單元測試本質上就是程式碼的使用說明書,它記錄了程式碼應該如何被使用、在什麼情境下會產生什麼結果。
一個好的測試方法名稱應該像說明書一樣清楚表達測試的意圖,讓任何人都能從名稱就理解這個測試在驗證什麼。更重要的是,當測試失敗時,開發者能夠立即從測試報告中理解是什麼功能出問題了。
想像一下,如果有人要使用你的程式碼,他們會想知道什麼?
測試方法的名稱就應該回答這些問題。
推薦的命名規範:被測試方法名稱_測試情境_預期行為
public class CalculatorTests
{
// 這個測試告訴使用者:Add 方法接受兩個整數,會回傳它們的和
[Fact]
public void Add_輸入1和2_應回傳3()
{
// Arrange
var calculator = new Calculator();
var a = 1;
var b = 2;
var expected = 3;
// Act
var result = calculator.Add(a, b);
// Assert
Assert.Equal(expected, result);
}
// 這個測試告訴使用者:Divide 方法遇到除數為0時會拋出例外
[Fact]
public void Divide_輸入10和0_應拋出DivideByZeroException()
{
// Arrange
var calculator = new Calculator();
var dividend = 10;
var divisor = 0;
// Act & Assert
Assert.Throws<DivideByZeroException>(() => calculator.Divide(dividend, divisor));
}
}
// X - 糟糕的說明書:無法理解使用方式
[Fact]
public void Test1() { } // 這告訴使用者什麼?
[Fact]
public void TestCalculator() { } // 測試計算機的什麼功能?
[Fact]
public void ShouldWork() { } // 什麼應該要工作?
// O - 優秀的說明書:清楚的使用指南
[Fact]
public void Add_輸入正數1和2_應回傳3()
{
// 使用者立即知道:Add方法接受正數,會回傳正確的和
}
[Fact]
public void Add_輸入負數和正數_應回傳正確結果()
{
// 使用者知道:Add方法也能處理負數
}
[Fact]
public void GetUser_輸入不存在的ID_應回傳null()
{
// 使用者知道:當ID不存在時,GetUser會回傳null
}
[Fact]
public void SaveUser_輸入null物件_應拋出ArgumentNullException()
{
// 使用者知道:SaveUser不接受null物件,會拋出例外
}
當測試失敗時,好的命名讓你立即知道:
❌ Test Failed: SaveUser_輸入null物件_應拋出ArgumentNullException
從這個失敗訊息,你立即知道:
SaveUser
輸入null物件
應拋出ArgumentNullException
相對於:
❌ Test Failed: Test1
這個失敗訊息完全沒有提供任何有用資訊,你需要打開程式碼才能理解測試的意圖。
記住:測試名稱是你留給未來自己和團隊成員的使用說明書。寫得越清楚,維護成本就越低。
在 AI 輔助開發的時代,我們需要重新思考測試的價值:
讓我們用一個簡單的例子來實踐今天學到的概念:
// 要測試的類別
public class StringHelper
{
public bool IsValidEmail(string email)
{
if (string.IsNullOrWhiteSpace(email))
{
return false;
}
return email.Contains("@") && email.Contains(".");
}
}
// 遵循 FIRST 原則的測試 (使用 xUnit)
public class StringHelperTests
{
private readonly StringHelper _helper;
public StringHelperTests()
{
_helper = new StringHelper(); // 每個測試都有新的實例
}
[Fact] // Fast: 不依賴外部資源
public void IsValidEmail_輸入有效Email_應回傳True()
{
// Arrange
var email = "test@example.com";
// Act
var result = _helper.IsValidEmail(email);
// Assert
Assert.True(result); // Self-Validating
}
[Fact] // Independent: 不依賴其他測試
public void IsValidEmail_輸入null值_應回傳False()
{
// Arrange
string email = null;
// Act
var result = _helper.IsValidEmail(email);
// Assert
Assert.False(result);
}
[Fact] // Repeatable: 每次執行結果都一樣
public void IsValidEmail_輸入空字串_應回傳False()
{
// Arrange
var email = "";
// Act
var result = _helper.IsValidEmail(email);
// Assert
Assert.False(result);
}
[Theory] // 使用 xUnit Theory 測試多個案例
[InlineData("invalid-email")]
[InlineData("@example.com")]
[InlineData("test@")]
[InlineData("test.example.com")]
[InlineData(" ")]
public void IsValidEmail_輸入無效Email格式_應回傳False(string invalidEmail)
{
// Arrange & Act
var result = _helper.IsValidEmail(invalidEmail);
// Assert
Assert.False(result);
}
[Theory] // 使用 Theory 測試多個有效案例
[InlineData("test@example.com")]
[InlineData("user.name@domain.org")]
[InlineData("admin@company.co.uk")]
public void IsValidEmail_輸入有效Email格式_應回傳True(string validEmail)
{
// Arrange & Act
var result = _helper.IsValidEmail(validEmail);
// Assert
Assert.True(result);
}
}
在開始明天的 xUnit 實作之前,請思考:
老派工程師的挑戰:找出你專案中最慢的一個測試,思考如何用單元測試來替代它的核心驗證邏輯。
被測試方法名稱_測試情境_預期行為
- 讓測試自說明明天我們將介紹 xUnit 框架的基礎使用,包括:
測試不是負擔,而是自由。當你有了完整的測試覆蓋,你就有了重構的勇氣、擴展的信心,以及面對變更需求時的從容。
在這個 AI 輔助開發的時代,理解測試的本質比會寫測試程式碼更重要。因為 AI 可以幫你寫程式碼,但只有你能決定要測試什麼,以及在測試金字塔的哪一層進行測試。
記住:好的測試策略,從建立穩固的金字塔基座開始。
對於程式開發的單元測試,在我開始以 C# 進行系統開發的時候就一直聽到,甚至於當時候的工作環境也一直想要推行,但是在 20 年前的那個時候,一堆人用 C# 程式語言然後建立好多類別就以為有在使用 物件導向開發
的那個時候,一堆人聽到說要做單元測試就直接皺眉頭,甚至於是說 你是時間太多、太閒嗎?
到了 2013 年 11 月 29 日這一天,twMVC 邀請了 91 哥以主題 如何在實務上使用 TDD 來開發
用生動、活潑的方式來講述 TDD,並且直接用 Living Demo 的方式演示了怎麼在一個 ASP.NET WebForm 的程式碼上,逐步地重構、抽介面、加入單元測試,一連串的操作行雲流水並且帶上說明,在此刻我才瞭解到什麼是重構舊程式與使用單元測試讓重構後的系統有了 保護
。
而後 2014 年開始 91 哥在 SkillTree 開了好幾個梯次的 自動測試與 TDD 實務開發(使用C#)
課程,我連上了五個梯次,每次上課都是滿滿的收穫,雖然每次的課程內容都是一樣,但是每個梯次上課的學員都不同,每次所提出來的問題也都不相同,但是 91 哥還是能夠一一地詳實回答,這都是累積難能可貴的實務經驗,尤其是下課後學員排隊等著向 91 哥提出課堂上的問題或是工作上所遇到的問題,我光是在旁邊站著聽都能夠學到好多。
這就是對測試的最開始的啟蒙學習過程。而之後如何在工作專案上使用,以及怎麼在公司裡慢慢推廣,都有好多可以講,就留待之後的每日主題的最後來慢慢分享。
相關連結:
範例程式碼:
這是「重啟挑戰:老派軟體工程師的測試修練」系列的第一天。如果你也是一個正在學習測試的工程師,歡迎分享你的想法和經驗!